package com.andfo.testframework;

import java.io.*;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.net.InetAddress;
import java.net.UnknownHostException;

import com.thoughtworks.selenium.Selenium;
import junit.framework.TestCase;
import junit.framework.TestResult;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.dbunit.database.IDatabaseConnection;

/**
 * User: andfo
 * Date: Jul 9, 2009
 * 
 * Provides the ability to store the data from any successful test. The class will also inspect any
 * dependency and run them if necessary. If a file from any dependant test exist, then this will
 * be restored instead of running the test. If the last successful test is the same as the
 * dependency, then no restore will be done in order to save time.
 */
public abstract class RepeatableSeleneseTestCase extends TestCase {

    public static final String CONF_FILE_PROPERTY = "conf.file";
    private static Set<String> completedTests = new TreeSet<String>();

    private String baseUrl;
    protected String contextPath;
    protected String browserString;
    private String seleniumServerHost;
    private int seleniumServerPort;

    private boolean pauseOnError;
    private boolean runAllOnce;

    protected long pauseOnErrorTime = 30000;
    protected DataStore dataStore;

    protected String appServerHost;
    protected int jmxPort;

    protected String appServerHostUser;
    protected String appServerHostPassword;

    private boolean dbMigration;
    private String migrationScriptBasePath;
    private String migrationScript;


    private boolean dateIsChanged;

    Log log = LogFactory.getLog(getClass().getName());

    private static String lastSuccessfulTest;
    protected Properties properties;


    private RemotableSeleneseTestBase stb = new RemotableSeleneseTestBase();

    /**
     * Use this object to run all of your selenium tests
     */
    protected Selenium selenium;

    /**
     * We need this constructor to enforce subclasses to use it.
     *
     * @param name Name of the method to run
     */
    public RepeatableSeleneseTestCase(String name) {
        super(name);
    }

    /**
     * This method will check if we need to run any dependencies and calls them. It will also make a decision
     * if we should restore a file.
     *
     * @throws Exception
     */
    @Override
    public void setUp() throws Exception {

        // Read the properties
        readProperties();

        /*
         * Run any dependency first (Unless result file already exists)
         */
        if (this.getClass().getMethod(getName()).isAnnotationPresent(DependsOnTest.class)) {
            DependsOnTest dependsOn = this.getClass().getMethod(getName()).getAnnotation(DependsOnTest.class);

            // Since it's not possible to have a class defaulting to null in an annotation, I got Class.class as default. Which we interpret at this class.
            Class dependencyTestClass = (dependsOn.testClass().equals(Class.class) ? getClass() : dependsOn.testClass());
            Method dependencyTestMethod = dependencyTestClass.getMethod(dependsOn.testMethod());
            log.info(getRestoreName(getClass(), getName()) + " depends on " + getRestoreName(dependencyTestClass, dependencyTestMethod));


            // If dependent file exist, then don't run the test (The file will be restored instead below)
            if (!dataStore.restoreFileExists(getRestoreName(dependencyTestClass, dependencyTestMethod))
                    || (runAllOnce && !completedTests.contains(getRestoreName(dependencyTestClass, dependencyTestMethod)))) {
                // Run dependent test case
                TestCase testCase = (TestCase) dependencyTestClass.getConstructor(String.class).newInstance(dependencyTestMethod.getName());
                TestResult testResult = testCase.run();

                completedTests.add(getRestoreName(dependencyTestClass, dependencyTestMethod));

                if (testResult.wasSuccessful()) {
                    log.info("Dependent test " + getRestoreName(dependencyTestClass, dependencyTestMethod) + " was successful.");
                    // Record the last successful test
                    lastSuccessfulTest = getRestoreName(dependencyTestClass, dependencyTestMethod);
                } else {
                    log.error("Dependent test " + getRestoreName(dependencyTestClass, dependencyTestMethod) + " was unsuccessful.");
                    throw new DependencyException("Dependent test " + getRestoreName(dependencyTestClass, dependencyTestMethod) + " was unsuccessful.");
                }
            } else {
                // If the restore file exists, then restore it (unless previous result was successful)
                if (!getRestoreName(dependencyTestClass, dependencyTestMethod).equals(lastSuccessfulTest)) {
                    loadRestoreFile(getRestoreName(dependencyTestClass, dependencyTestMethod));
                } else {
                    log.info("Dependent test was successful, no need to restore dependent class.");
                }
            }
        }

        if (this.getClass().getMethod(getName()).isAnnotationPresent(DependsOnData.class)) {
            DependsOnData dataDependency = (DependsOnData) this.getClass().getMethod(getName()).getAnnotation(DependsOnData.class);

            // Recreate the database if this is specified
            if(dataDependency.recreateDb()) {
                dataStore.recreateDb();
            }

            /*
             * If it's dependent on a restore file, then restore it (or them).
             */
            if (dataDependency.restoreFile().length != 0) {
                for (String restoreFile : dataDependency.restoreFile()) {
                    log.info("Loading restore file:" + restoreFile);
                    if (!dataStore.restoreFileExists(restoreFile)) {
                        throw new DependencyException("Required file does not exist:" + restoreFile);
                    }
                    loadRestoreFile(restoreFile);
                }
            }

            /*
             * Check if we need a dbUnit file.
             */
            if (dataDependency.dbUnitFile().length != 0) {
                for (String dbUnitFileName : dataDependency.dbUnitFile()) {
                    File dbUnitFile = new File(dbUnitFileName);
                    log.info("Loading dbUnit file:" + dbUnitFile.getAbsolutePath());
                    if (!dbUnitFile.exists()) {
                        throw new DependencyException("Required file does not exist:" + dbUnitFile.getAbsolutePath());
                    }

                    IDataSet dataSet = new FlatXmlDataSet(dbUnitFile);
                    IDatabaseConnection connection = dataStore.createConnection();
                    try {
                        DatabaseOperation.INSERT.execute(connection, dataSet);
                    } finally {
                        connection.close();
                    }
                }
            }


        }

        log.info("STARTING TEST:" + getRestoreName(getClass(), getName()));

        stb.setUp(baseUrl, browserString, seleniumServerPort, seleniumServerHost);
        selenium = stb.getSelenium();

    }

    /**
     * Reads the property file and instantiate the datastore
     * Fist check if a file have been specified explicitly
     * Second try to load from current directory.
     * Third just use system properties if specified, for ones that don't. Use default.
     */
    protected void readProperties() {

        // Load the property file
        File properyFile;
        if(System.getProperty(CONF_FILE_PROPERTY) == null || System.getProperty(CONF_FILE_PROPERTY).length() == 0) {
            properyFile = new File("./conf.properties");
        } else {
            properyFile = new File(System.getProperty(CONF_FILE_PROPERTY));
        }

        if (properyFile.exists()) {
            log.info("Loading properties from file:" + properyFile.getAbsolutePath());
            properties = new Properties();
            try {
                properties.load(new FileReader(properyFile));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            log.info("Loading properties from system properties");
            properties = System.getProperties();
        }

        baseUrl = properties.getProperty("baseUrl", "http://localhost:8080");
        contextPath = properties.getProperty("contextPath", "/helios");
        browserString = properties.getProperty("browserString", "*firefox");
        pauseOnError = "true".equals(properties.getProperty("pauseOnError", "false"));
        seleniumServerPort = Integer.parseInt(properties.getProperty("seleniumServerPort", "4444"));
        seleniumServerHost = properties.getProperty("seleniumServerHost", "localhost");
        runAllOnce = "true".equals(properties.getProperty("runAllOnce", "false"));

        appServerHost = properties.getProperty("appServerHost", "localhost");
        jmxPort = Integer.parseInt(properties.getProperty("jmxPort", "9999"));

        appServerHostUser = properties.getProperty("appServerHostUser", "root");
        appServerHostPassword = properties.getProperty("appServerHostPassword");

        dbMigration="true".equals(properties.getProperty("dbMigration", "false"));
        migrationScriptBasePath = properties.getProperty("migrationScriptBasePath", ".");
        migrationScript = properties.getProperty("migrationScript", "changes.xml");

        String dataStoreClass = properties.getProperty("dataStoreClass", "com.andfo.testframework.MySqlDataStore");

        // Initiate the datasore
        try {
            dataStore = (DataStore) Class.forName(dataStoreClass).newInstance();
            dataStore.init(properties);
        } catch (Exception e) {
            throw new RuntimeException("Exceptino while initiating the datastore", e);
        }


    }


    /**
     * Used to created the restore file when a test has been successfully run.
     *
     * @throws Throwable
     */
    @Override
    protected void runTest() throws Throwable {
        try {
            super.runTest();

            // If we succeded, create a restore point.
            lastSuccessfulTest = getRestoreName(getClass(), getName());
            createRestoreFile(getRestoreName(getClass(), getName()));
        } catch (Throwable e) {
            log.error("Test threw an error", e);
            if (pauseOnError) {
                log.info("Pausing on error is set to true, pausing for: " + (pauseOnErrorTime / 1000) + "s");
                Thread.sleep(pauseOnErrorTime);
            }
            throw e;
        }
    }


    @Override
    protected void tearDown() throws Exception {
        stb.tearDown();
        super.tearDown();
        if(dateIsChanged) resetDate();
    }

    /**
     * Restores the file called base
     */
    protected void loadDefaultRestoreFile() {
        loadRestoreFile("base");
    }

    /**
     * Added in order for subclasses to get notified when a restore is done
     *
     * @param restoreName Name of the restore file
     */
    protected void loadRestoreFile(String restoreName) {
        dataStore.loadRestoreFile(restoreName);
        if(dbMigration) {
            DbMigrator dbMigrator = new DbMigrator();
            dbMigrator.setDataSource(dataStore.getDataSource());
            dbMigrator.doMigration(migrationScriptBasePath, migrationScript);
        }
    }

    /**
     * Added in order for subclasses to get notified when a restore file is created
     *
     * @param restoreName Name of the restore file
     */
    protected void createRestoreFile(String restoreName) {
        dataStore.createRestoreFile(restoreName);
    }

    /**
     * Utility method to create a name for each test method. It combines the clas name + # + method name.
     *
     * @param testClass  The test class
     * @param testMethod The test method
     * @return A combined name
     */
    protected String getRestoreName(Class testClass, Method testMethod) {
        return getRestoreName(testClass, (testMethod == null ? null : testMethod.getName()));
    }

    /**
     * Returns a connection to the database that can be used to inspect data elements.
     *
     * @return
     */
    public Connection createDatabaseConnection() {
        try {
            return dataStore.createConnection().getConnection();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * Utility method to create a name for each test method. It combines the clas name + # + method name.
     *
     * @param testClass  The test class
     * @param testMethod Name of the test method
     * @return A combined name
     */
    protected String getRestoreName(Class testClass, String testMethod) {
        return testClass.getName() + "#" + testMethod;
    }


    /**
     * Utility method for changing the date on the application server. This version assumes that the
     * server is a linux server. This can be improved. Time cannot be changed at this time. Let me know if you need it.
     * If date is changed during a test, it will automatically be reset after the end of that test.
     * @param date The date that the test server should have
     */
    public void setDate(String date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        SimpleDateFormat linuxFormat = new SimpleDateFormat("MMddHHmmyyyy");
        String command = null;
        try {
            command = "sudo date " + linuxFormat.format(sdf.parse(date));
            executeOnAppServer(command);
        } catch (ParseException e) {
            throw new RuntimeException("Wrong date format");
        } finally {
            // Assume that the date was changed (just in case)
            dateIsChanged = true;
        }
    }

    /**
     * Uses ntpdate in order to try to reset the date to current date. (implies linux)
     */
    public void resetDate() {
        try {
            String command = "sudo ntpdate 0.us.pool.ntp.org";
            executeOnAppServer(command);
        } catch (Exception e) {
            log.error("Unable to reset date", e);
            // Don't keep throwing. Just let it go.
        }
    }

    /**
     * Checks to see if the app server is defined as localhost or 127.0.0.1. In that case, run
     * command locally. Otherwise use ssh (implies linux server)
     * @param command The command to run
     */
    public void executeOnAppServer(String command) {
        try {
            if(InetAddress.getByName(appServerHost).isLoopbackAddress()) {
                executeSystemCommand(command);
            } else {
                SshExec ssh = SshExec.getConnection(appServerHostUser, appServerHostPassword, appServerHost);
                String result = ssh.exec(command);
                log.info(result);
            }
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Executes a system command
     * @param command
     */
    public static void executeSystemCommand(String command) {
        try {
            Process p = Runtime.getRuntime().exec(command);
            AsyncStreamReader.startReader(p.getInputStream(), "INPUT");
            AsyncStreamReader.startReader(p.getErrorStream(), "ERROR");
            p.waitFor();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
